package link.luyu.plugin.brochain;

import java.math.BigInteger;
import java.util.*;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import link.luyu.plugin.brochain.types.BlockAndPage;
import link.luyu.plugin.brochain.web3.methods.response.Block;
import link.luyu.plugin.brochain.web3.methods.response.Page;

public class Cache {

    private static final int CAPACITY = 1000;
    private static final int BLOCK_LIMIT = 1000;

    private BigInteger latestBlockNumber;

    private final Map<String, BlockAndPage> blockAndPages;
    private final NavigableMap<BigInteger, String> blockHashes;
    private final Map<String, TransactionIndex> transactionIndices;

    private final ReadWriteLock lock;

    public Cache() {
        this.latestBlockNumber = BigInteger.ZERO;
        this.blockAndPages = new HashMap<>();
        this.blockHashes = new TreeMap<>();
        this.transactionIndices = new HashMap<>();
        this.lock = new ReentrantReadWriteLock();
    }

    public BigInteger getLatestBlockNumber() {
        lock.readLock().lock();
        BigInteger result = latestBlockNumber;
        lock.readLock().unlock();
        return result;
    }

    public BlockAndPage getBlockAndPageByHash(String blockHash) {
        lock.readLock().lock();
        BlockAndPage result = blockAndPages.get(blockHash);
        lock.readLock().unlock();
        return result;
    }

    public BlockAndPage getBlockAndPageByNumber(BigInteger blockNumber) {
        lock.readLock().lock();
        BlockAndPage result = null;
        String blockHash = blockHashes.get(blockNumber);
        if (blockHash != null) {
            result = blockAndPages.get(blockHash);
        }
        lock.readLock().unlock();
        return result;
    }

    public Page.PageTransaction getPageTransactionByHash(String transactionHash) {
        lock.readLock().lock();
        Page.PageTransaction result = null;
        TransactionIndex transactionIndex = transactionIndices.get(transactionHash);
        if (transactionIndex != null) {
            BlockAndPage blockAndPage = blockAndPages.get(transactionIndex.blockHash);
            if (blockAndPage != null) {
                result =
                        blockAndPage
                                .getPage()
                                .getTransactions()
                                .get(transactionIndex.transactionIndex);
            }
        }
        lock.readLock().unlock();
        return result;
    }

    public boolean updateLatestBlockNumber(BigInteger blockNumber) {
        lock.writeLock().lock();
        try {
            if (blockNumber.compareTo(latestBlockNumber) <= 0) {
                return false;
            }
            latestBlockNumber = blockNumber;
        } finally {
            lock.writeLock().unlock();
        }
        return true;
    }

    public boolean addBlockAndPage(BlockAndPage blockAndPage) {
        lock.writeLock().lock();
        try {
            // Check block number
            Block block = blockAndPage.getBlock();
            if (block.getNumber().compareTo(latestBlockNumber) > 0
                    || block.getNumber()
                                    .compareTo(
                                            latestBlockNumber.subtract(
                                                    BigInteger.valueOf(BLOCK_LIMIT)))
                            < 0) {
                return false;
            }
            // Remove oldest block and page
            if (blockAndPages.size() >= CAPACITY) {
                String oldestBlockHash = blockHashes.pollFirstEntry().getValue();
                BlockAndPage oldestBlockAndPage = blockAndPages.remove(oldestBlockHash);
                for (Page.PageTransaction transaction :
                        oldestBlockAndPage.getPage().getTransactions()) {
                    transactionIndices.remove(transaction.getHash());
                }
            }
            // Add block and page
            String blockHash = block.getHash();
            blockAndPages.put(blockHash, blockAndPage);
            blockHashes.put(block.getNumber(), blockHash);
            Page page = blockAndPage.getPage();
            if (page != null) {
                List<Page.PageTransaction> transactions = page.getTransactions();
                for (int i = 0; i < transactions.size(); i++) {
                    transactionIndices.put(
                            transactions.get(i).getHash(), new TransactionIndex(blockHash, i));
                }
            }
        } finally {
            lock.writeLock().unlock();
        }
        return true;
    }

    private static class TransactionIndex {

        private final String blockHash;
        private final int transactionIndex;

        private TransactionIndex(String blockHash, int transactionIndex) {
            this.blockHash = blockHash;
            this.transactionIndex = transactionIndex;
        }
    }
}
